/*
* Copyright (C) Microsoft. All rights reserved.  
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not 
* use this file except in compliance with the License.  You may obtain a copy 
* of the License at http://www.apache.org/licenses/LICENSE-2.0  
* 
* THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 
* WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 
* MERCHANTABLITY OR NON-INFRINGEMENT. 
*
* See the Apache License, Version 2.0 for specific language governing 
* permissions and limitations under the License.
*
*/
#include <baja/app/var_console.hpp>

#include <baja/common.hpp>
#include <baja/graphics.hpp>
#include <baja/math.hpp>
#include <boost/format.hpp>
#include <baja/observable.hpp>

namespace baja { namespace app {

namespace b = baja;
namespace g = baja::graphics;
namespace m = baja::math;

BAJA_BEGIN_CONSTANTS(var_console_constants)
    BAJA_DECLARE_CONST(b::uint32, currentGroupIndex, 0)
    // TRACK: 82: BAJA_DECLARE_CONST(b::uint32, saveIndex, 10)
    // TRACK: 82: BAJA_DECLARE_CONST(b::uint32, loadIndex, 20)
    BAJA_DECLARE_CONST(b::uint32, backIndex, 1)
    BAJA_DECLARE_CONST(b::uint32, firstVarIndex, 2) // TRACK: remove load and save altogether
    BAJA_DECLARE_CONST(b::int32, totalConsoleLinesAsInt, 22)
    BAJA_DECLARE_CONST(b::uint32, consoleFontEmHeight, 20)
    BAJA_DECLARE_CONST(b::float32, consoleFontPixelHeight, 15.f)
    BAJA_DECLARE_CONST(b::float32, spacingBetweenLines, 2.5f)
    BAJA_DECLARE_CONST(b::float32, totalConsoleLines, (b::float32)var_console_constants::totalConsoleLinesAsInt)
    BAJA_DECLARE_CONST(b::float32, controlGroupSize, 0.f)
    BAJA_DECLARE_CONST(b::float32, leftMargin, var_console_constants::controlGroupSize + 8.f)
BAJA_END_CONSTANTS

// constants
BAJA_BEGIN_GLOBALS(var_console_globals)
    BAJA_DECLARE_CONST_GLOBAL(b::wchar*, itemIsSelected)
    BAJA_DECLARE_CONST_GLOBAL(b::wchar*, itemIsNotSelected)
BAJA_END_GLOBALS(var_console_globals)

BAJA_DEFINE_CONST_GLOBAL(var_console_globals, b::wchar*, itemIsSelected, L"==> ")
BAJA_DEFINE_CONST_GLOBAL(var_console_globals, b::wchar*, itemIsNotSelected, L"    ")

class var_console : public ivar_console
{
public:
    
    var_console() : 
           _selection(var_console_constants::firstVarIndex), 
           _isVisible(false) 
       {}

    enum class toggle_type
    {
        increment,
        decrement,
        select,
    };

    struct history_entry
    {
        std::weak_ptr<b::ivar_group> group;
        size_t selectedItem;
    };
    
    static std::shared_ptr<var_console> create(const std::shared_ptr<b::ivar_group>& root)
    {
        std::shared_ptr<var_console> returnValue = std::make_shared<var_console>();
        returnValue->initialize(root);
        return returnValue;
    }

    void onAddedToRenderService(const std::shared_ptr<g::irender_service>& renderService)
    {
    }

    void onRemovedFromRenderService(const std::shared_ptr<g::irender_service>& renderService)
    {
    }

    std::shared_ptr<b::ivar_group> getRoot()
    {
        return _root.lock();
    }

    bool getIsVisible()
    {
        return _isVisible;
    }

    void setIsVisible(bool isVisible)
    {
        _isVisible = isVisible;

        // if turning off, forget all about our row data
        if (!isVisible)
        {
            _rowData.clear();
        }
    }

    b::int32 getPriority()
    {
        // var console should always be on top of everything else
        return INT_MAX;
    }

    void render(const std::shared_ptr<g::iengine>& graphics)
    {
        if (_isVisible)
        {
            // load font when needed
            if (_consoleFont.get() == nullptr)
            {
                // get asset manager
                std::shared_ptr<g::iasset_manager> assetManager = graphics->getAssetManager();
                _consoleFont = assetManager->getOrCreateFont(
                    graphics, 
                    L"Consolas",
                    var_console_constants::consoleFontEmHeight,
                    false
                    );
            }

            this->verifyCurrentGroup();

            // make sure normal render mode is being used
            g::render_mode originalRenderMode = graphics->getRenderMode();
            graphics->setRenderMode(g::render_mode::normal);

            graphics->setZBuffer(g::enabled_state::disabled);

            g::viewport vp;
            graphics->getViewport(&vp);

            this->renderBackground(graphics, vp);
            this->renderVars(graphics, vp);
         
            // restore render mode back to original state
            graphics->setRenderMode(originalRenderMode);
        }
    }

    void renderBackground(const std::shared_ptr<g::iengine>& graphics, const g::viewport& vp)
    {
        std::shared_ptr<g::ieffect2> effect;

        graphics->setTexture(0, nullptr);

        effect = graphics->getStandardEffect(g::standard_effect::screenColored);

        effect->begin(graphics);

        graphics->setZBuffer(g::enabled_state::disabled);

        graphics->beginScreenSpace(g::primitive_type::triangleStrip);
        graphics->setColor(g::packColor(0xa0, 0x00, 0x00, 0x00));

        m::point4d point;
            
        point.set(-0.5f, (var_console_constants::consoleFontPixelHeight * var_console_constants::totalConsoleLines)-0.5f, 0.0f);
        graphics->setScreenSpaceVertex(point);
            
        point.set(-0.5f, -0.5f, 0.0f);
        graphics->setScreenSpaceVertex(point);
            
        point.set(vp.width - 0.5f,  
            (var_console_constants::consoleFontPixelHeight * var_console_constants::totalConsoleLines)-0.5f, 0.0f);
        graphics->setScreenSpaceVertex(point);

        point.set(vp.width - 0.5f, -0.5f, 0.0f);
        graphics->setScreenSpaceVertex(point);

        graphics->end();

        graphics->setZBuffer(g::enabled_state::enabled);

        effect->end(graphics);
    }

    // render the vars
    void renderVars(const std::shared_ptr<g::iengine>& graphics, const g::viewport& vp)
    {
        // build up the row data, as needed
        if (_rowData.size() == 0)
        {
            this->buildRowData();
        }

        // only continue if we have data to display
        if (_rowData.size() > 0)
        {
            // we render from top row first
            b::float32 row = 0.0f;

            // adjust selection to be valid
            _selection = min(_selection, _rowData.size() - 1);

            // determine start and end of data to display - try to keep selection in
            // middle of the display...
            long long halfLength = var_console_constants::totalConsoleLinesAsInt / 2;
            long long start = _selection - halfLength;
            long long end = _selection + halfLength;

            // adjust start and end is selection is too close to either
            if (start < 0)
            {
                end += -start;
                start = 0;
            }
            if (end > (b::int32)_rowData.size())
            {
                start -= end - (b::int32)_rowData.size();
            }

            // cap start and end to valid range
            start = max(start, 0);
            end = min(end, (b::int32)_rowData.size());

            for(b::uint32 i = (b::uint32)start; i < (b::uint32)end; i++)
            {
                std::shared_ptr<b::ivar_wrapper> var = std::dynamic_pointer_cast<b::ivar_wrapper>(_rowData[i].lock());
                std::shared_ptr<b::ivar_group> group = std::dynamic_pointer_cast<b::ivar_group>(_rowData[i].lock());

                // note: it is valid for group to be NULL if we're <= the parent group index
                if (group.get() != nullptr|| i < var_console_constants::firstVarIndex)
                {
                    // render group info
                    this->renderGroup(graphics, group, i, row, vp);
                }
                else if (var != NULL)
                {
                    // render var
                    this->renderVar(graphics, var, i, row, vp);
                }

                row += var_console_constants::consoleFontPixelHeight;
            }
        }
    }

    void renderGroup(
        const std::shared_ptr<g::iengine>& graphics,
        const std::shared_ptr<b::ivar_group>& group, 
        const b::uint32 position, 
        const b::float32 row,
        const g::viewport& vp
        )
    {
        std::wstring textToDisplay;

        const b::wchar* selectionString = (_selection == position ? 
            var_console_globals::itemIsSelected : var_console_globals::itemIsNotSelected);

        if (position == var_console_constants::currentGroupIndex)
        {
            // current group
            std::wstring groupName;
            groupName = group->getName();
            textToDisplay = (boost::wformat(L"GROUP: %s") % groupName.c_str()).str();
        }
        // TRACK: 82: remove load/save
        /*
        else if (position == var_console_constants::saveIndex)
        {
            textToDisplay = (boost::wformat(L"%sSAVE") % selectionString).str();
        }
        else if (position == var_console_constants::loadIndex)
        {
            textToDisplay = (boost::wformat(L"%sLOAD") % selectionString).str();
        }
        */
        else if (position == var_console_constants::backIndex)
        {
            textToDisplay = (boost::wformat(L"%s[..]") % selectionString).str();
        }
        else if (group != nullptr)
        {
            std::wstring groupName = group->getName();
            textToDisplay = (boost::wformat(L"%s[%s]") % selectionString % groupName.c_str()).str();
        }

        this->renderRow(graphics, textToDisplay.c_str(), row, 0, vp);
    }

    // render var data
    void renderVar(
        const std::shared_ptr<g::iengine>& graphics,
        const std::shared_ptr<b::ivar_wrapper>& var, 
        const b::uint32 position, 
        const b::float32 row,
        const g::viewport& vp
        )
    {
        if (var != nullptr)
        {
            std::wstring varName = var->getName();
            std::wstring varValue = var->getDataAsString();

            std::wstring textToDisplay;
            const b::wchar* selectionString = (_selection == position ? 
                var_console_globals::itemIsSelected : var_console_globals::itemIsNotSelected);

            textToDisplay = (boost::wformat(L"%s%s : %s")
                % selectionString
                % varName.c_str()
                % varValue.c_str()
                ).str();

            this->renderRow(graphics, textToDisplay.c_str(), row, var->getFlags(), vp);
        }
    }

    // render a row of text
    void renderRow(
        const std::shared_ptr<g::iengine>& graphics,
        const b::wchar* textToDisplay,
        const b::float32 row,
        const int flags,
        const g::viewport& vp
        )
    {
        g::packed_color color;

        if ((flags & b::varFlagReadOnly) == b::varFlagReadOnly)
        {
            color = g::packColor(0xff, 0xd0, 0xd0, 0xd0);
        }
        else
        {
            color = g::packColor(0xff, 0xff, 0xff, 0xff);
        }

        m::rectangle rect(
            var_console_constants::leftMargin, 
            row, 
            vp.width, 
            row + var_console_constants::consoleFontPixelHeight
            );

        // draw text
        _consoleFont->renderText(
            graphics,
            textToDisplay,
            color,
            g::horizontal_alignment::left,
            g::vertical_alignment::top,
            &rect
            );
    }

    void buildRowData()
    {
        this->verifyCurrentGroup();

        // make sure we clear all current row data
        _rowData.clear();

        std::shared_ptr<b::ivar_group> currentGroup = _currentGroup.lock();

        // first element is always the group we are in
        std::weak_ptr<b::ibase> base = std::dynamic_pointer_cast<b::ibase>(currentGroup);
        _rowData.push_back(base);

        // next set of entries are meant for actions
        _rowData.push_back(std::weak_ptr<b::ibase>());
        // TRACK: 82: remove load/save _rowData.push_back(std::weak_ptr<b::ibase>());
        // TRACK: 82: remove load/save _rowData.push_back(std::weak_ptr<b::ibase>());

        // add all child groups
        size_t count = _currentGroup.lock()->getChildGroupCount();

        for (b::uint32 i = 0; i < count; i++)
        {
            std::shared_ptr<b::ivar_group> child;
            child = currentGroup->getChildGroup(i);

            std::weak_ptr<b::ibase> base = std::dynamic_pointer_cast<b::ibase>(child);
            _rowData.push_back(base);
        }

        // add all vars
        count = currentGroup->getVarCount();

        for (b::uint32 i = 0; i < count; i++)
        {
            std::shared_ptr<b::ivar_wrapper> var = currentGroup->getVar(i);
            std::weak_ptr<b::ibase> base = std::dynamic_pointer_cast<b::ibase>(var);
            _rowData.push_back(base);
        }
    }

    // moves selection up
    void moveSelectionUp()
    {
        if (_isVisible)
        {
            // can't move up past the parent group index
            // TRACK: 82: remove load/save
            if (_selection > 1) //var_console_constants::saveIndex)
            {
                _selection--;
            }
            else
            {
                _selection = _rowData.size() - 1;
            }
        }
    }

    // moves selection down
    void moveSelectionDown()
    {
        if (_isVisible)
        {
            if (_selection < _rowData.size() - 1)
            {
                _selection++;
            }
            else
            {
                _selection = 1; // TRACK: 82: renmove load save var_console_constants::saveIndex;
            }
        }
    }

    // decrement selection
    void decrementSelection()
    {
        this->toggleSelection(toggle_type::decrement);
    }

    // increment selection
    void incrementSelection()
    {
        return this->toggleSelection(toggle_type::increment);
    }

    // select current item
    void activateSelection()
    {
        return this->toggleSelection(toggle_type::select);
    }

    void navigateBack()
    {
        if (!_history.empty())
        {
            // Move back to previous entry in history, and restore
            // the selection in that view
            history_entry historyEntry = _history.top();
            _currentGroup = historyEntry.group;
            _selection = historyEntry.selectedItem;
            _history.pop();
            _rowData.clear();
            this->verifyCurrentGroup();
        }
    }

    void toggleSelection(const toggle_type type)
    {
        this->verifyCurrentGroup();

        if (_isVisible)
        {
            // TRACK: 82: remove save load
            /*
            if (_selection == var_console_constants::saveIndex)
            {
                if (type == toggle_type::select)
                {
                    // this->save();
                }
            }
            else if (_selection == var_console_constants::loadIndex)
            {
                if (type == toggle_type::select)
                {
                    // this->load();
                }
            }
            */
            if (_selection == var_console_constants::backIndex)
            {
                if ((type == toggle_type::select) || (type == toggle_type::increment))
                {
                    this->navigateBack();
                }
            }
            else if (_selection < _rowData.size())
            {
                std::shared_ptr<b::ivar_wrapper> var = std::dynamic_pointer_cast<b::ivar_wrapper>(_rowData[_selection].lock());
                std::shared_ptr<b::ivar_group> group = std::dynamic_pointer_cast<b::ivar_group>(_rowData[_selection].lock());

                if (group.get() != nullptr)
                {
                    if ((type == toggle_type::select) || (type == toggle_type::increment))
                    {
                        // Save current state in history stack
                        history_entry history;
                        history.group = _currentGroup;
                        history.selectedItem = _selection;
                        _history.push(history);

                        // switch into this group
                        _currentGroup = group;
                        _rowData.clear();
                        _selection = var_console_constants::firstVarIndex;
                    }
                }
                else if (var != NULL)
                {
                    _observerSource.raise([&var, &group, this](const std::shared_ptr<ivar_console_observer>& observer, void* context)
                    {
                        observer->onBeforeVarChange(var, _currentGroup.lock());
                    });

                    if (type == toggle_type::increment)
                    {
                        var->increment();
                    }
                    else if (type == toggle_type::decrement)
                    {
                        var->decrement();
                    }
                    else
                    {
                        var->incrementAndWrap();
                    }

                    _observerSource.raise([&var, &group, this](const std::shared_ptr<ivar_console_observer>& observer, void* context)
                    {
                        observer->onAfterVarChange(var, _currentGroup.lock());
                    });
                }
            }
        }
    }

    int32 addObserver(const std::shared_ptr<ivar_console_observer>& observer)
    {
        return _observerSource += observer;
    }

    void removeObserver(const std::shared_ptr<ivar_console_observer>& observer)
    {
        _observerSource -= observer;
    }

    void removeObserver(const int32 cookie)
    {
        _observerSource -= cookie;
    }

private:
    void initialize(const std::shared_ptr<b::ivar_group>& root)
    {
        _root = root;
        _currentGroup = _root;
    }

    // verifies current group still valid, if not restores back to root group
    void verifyCurrentGroup()
    {
        std::shared_ptr<b::ivar_group> currentGroup = _currentGroup.lock();
        std::shared_ptr<b::ivar_group> root = _root.lock();

        // check current group - if it has no parent and it isn't the root
        // then we should throw it away and restart at the root position
        if (currentGroup.get() != nullptr && currentGroup.get() != root.get())
        {
            std::shared_ptr<b::ivar_group> parent = currentGroup->getParent();
            if (parent.get() == nullptr)
            {
                currentGroup = std::shared_ptr<b::ivar_group>();
            }
        }        

        // check row data for groups that might also be gone. if any are found,
        // restart back at the root position.
        if (currentGroup.get() != nullptr)
        {
            for(b::uint32 i = 0; i < _rowData.size(); i++)
            {
                std::shared_ptr<b::ivar_group> group = std::dynamic_pointer_cast<b::ivar_group>(_rowData[i].lock());
                if (group.get() != nullptr && group.get() != root.get())
                {
                    std::shared_ptr<b::ivar_group> parent = group->getParent();
                    if (parent.get() == nullptr)
                    {
                        currentGroup = std::shared_ptr<b::ivar_group>();
                        break;
                    }
                }
            }
        }

        // if current group is NULL then it means we have to restart 
        // back at the root
        if (currentGroup.get() == nullptr)
        {
            _rowData.clear();

            while (!_history.empty())
            {
                _history.pop();
            }

            _selection = var_console_constants::firstVarIndex;
            currentGroup = root;
        }

        _currentGroup = currentGroup;
    }

    std::vector<std::weak_ptr<b::ibase>> _rowData; 
    std::stack<history_entry> _history;
    std::shared_ptr<g::ifont> _consoleFont;
    std::weak_ptr<b::ivar_group> _root;
    std::weak_ptr<b::ivar_group> _currentGroup;
    size_t _selection;
    bool _isVisible;
    observable_source<ivar_console_observer> _observerSource;
};

inline std::shared_ptr<ivar_console> createVarConsole(const std::shared_ptr<b::ivar_group>& root)
{
    std::shared_ptr<var_console> returnValue = var_console::create(root);
    return std::dynamic_pointer_cast<ivar_console>(returnValue);
}

}} // namespace baja::app
